from __future__ import division
import astar
import types
import ctypes
import random
import rfc822
import math
import pyglet
from pyglet.gl import *
import vector
import game
import multitexture
import window


class Tile:
	"""Tile base class"""

	PRIORITY = 0 # Order in which to draw this tile type (higher == later)
	BASE_ELEVATION = 0 # Lowest elevation of this terrain
	ELEVATION_UNIT = 1 # Unit of elevation gain per key
	ELEVATION_DEVIATION = 0 # Random elevation deviation
	CLIP_ELEVATION = None # Max elevation where terrain texture is drawn
	PASSABLE = True # Can ground units travel over it?
	BUILDABLE = True # Can buildings be made on it?
	HOVERABLE = True # Can hovering units travel over it?
	FLYABLE = True # Can flying units travel over it?
	GROUND_SPEED = 1.0 # What's the speed multiplier for ground units?
	HOVER_SPEED = 1.0 # What's the speed multiplier for hover units?
	FLYING_SPEED = 1.0 # What's the speed multiplier for flying units?

	## Edge types ##
	SOFT_EDGE = 0
	MEDIUM_EDGE = 1
	HARD_EDGE = 2

	EDGE_BLEND = MEDIUM_EDGE # Hard or soft edge blend
	EDGE_OVERLAP = 0.8 # Overlap factor between adjacent tiles

	def __init__(self, key):
		"""Create map tile.

		key -- Map key character used to designate tile. Some tile
		classes may have more than one.

		position -- Tile x,y position on the map
		"""
		self.key = key
		self.position = None
		self.neighbors = [None] * 8
		self.explored = False
		self.elevation = (self.BASE_ELEVATION + self.KEY.find(key) * self.ELEVATION_UNIT 
			+ random.gauss(0, self.ELEVATION_DEVIATION))
	
	def set_neighbors(self, *neighbors):
		"""Set the 8 tiles neighboring this tile
		
		neighbors are the 8 surrounding tiles
		starting at the top and going clockwise:

		+---+---+---+
		| 7 | 0 | 1 |
		+---+---+---+
		| 6 | * | 2 |
		+---+---+---+
		| 5 | 4 | 3 |
		+---+---+---+

		If a tile is on an an edge, neighbors off the map are None.
		neighbor tiles are also mapped to convenience attributes named:
		above, above_right, right, below_right, below, below_left, left, above_left
		"""
		self.neighbors = neighbors
		self.above, self.above_right, self.right, self.below_right, \
			self.below, self.below_left, self.left, self.above_left = neighbors
	
	def set_position(self, x, y):
		"""Specify the position of the tile on the map"""
		self.position = (x, y)

	@property
	def location(self):
		return vector.Vec3(self.position[0], self.position[1], self.elevation)
	
	def __repr__(self):
		return '<%s 0x%x at %s>' % (self.__class__.__name__, id(self), self.position)


class BareGround(Tile):
	"""Flat bare ground terrain"""
	KEY = 'bB'
	PRIORITY = 2

class CoveredGround(Tile):
	"""Flat ground covered with grass, snow, etc"""
	KEY = 'cC'
	PRIORITY = 3
	EDGE_OVERLAP = 1.5
	EDGE_BLEND = Tile.SOFT_EDGE

class UnevenGround(Tile):
	"""Broken, uneven ground"""
	KEY = 'uU'
	PRIORITY = 4
	BUILDABLE = False
	GROUND_SPEED = 0.25
	EDGE_BLEND = Tile.SOFT_EDGE
	EDGE_OVERLAP = 1.2

class Boulder(Tile):
	"""Large boulder or debris"""
	KEY = '.oO@'
	PRIORITY = 5
	BASE_ELEVATION =  0.5
	ELEVATION_UNIT = 0.3
	EDGE_OVERLAP = 0.75
	EDGE_BLEND = Tile.SOFT_EDGE
	PASSABLE = False
	HOVERABLE = False
	BUILDABLE = False

class Ramp(Tile):
	"""Ramp between tile elevations"""
	KEY = 'rR'

class Minerals(Tile):
	"""Mineral resources"""
	KEY = 'mM'
	PRIORITY = 99
	PASSABLE = False
	BUILDABLE = False
	EDGE_BLEND = Tile.HARD_EDGE
	EDGE_OVERLAP = 0.5
	BASE_ELEVATION = 1.4
	ELEVATION_DEVIATION = 0.4

class GasVent(Tile):
	"""Gas resources"""
	KEY = 'gG'
	PRIORITY = 99
	PASSABLE = False
	BUILDABLE = False
	HOVER_SPEED = 0.5

class Water(Tile):
	"""Body of Water"""
	KEY = 'wW'
	PRIORITY = 0
	EDGE_BLEND = Tile.HARD_EDGE
	EDGE_OVERLAP = 0
	PASSABLE = False
	BUILDABLE = False
	BASE_ELEVATION =  -1.8
	CLIP_ELEVATION = BASE_ELEVATION * 0.85
	ELEVATION_UNIT = 0
	ELEVATION_DEVIATION = 0

class Mountain(Tile):
	"""High altitude mountain range"""
	KEY = 'pP'
	PRIORITY = 4
	PASSABLE = False
	BUILDABLE = False
	HOVERABLE = False
	FLYABLE = False
	BASE_ELEVATION =  1.75
	ELEVATION_UNIT = 1.25
	ELEVATION_DEVIATION = 0.35
	# XXX Mountains will also block visibility


class Map:
	"""Tileset populated from a map file
	
	A map file consists of 2 parts, a metadata header and a 
	tile key grid. The header consists of rfc822-style "key: value"
	pair lines. The following metadata keys are recognized:

	name -- Map short name
	description -- Longer friendly map description

	The headers ends with a blank line, which starts the tile key
	section. This section consists of lines of character keys designating
	the types of each tile on the map in the position of the character.
	Due to the isometric layout, even lines have one more tile than
	odd lines. Spaces in this section are ignored, so you can use them to
	align and offset the tile key characters in the file to make it
	easier to visualize the resulting map in text form.
	"""

	_key2tile = {}
	for obj in globals().values():
		if (isinstance(obj, (types.ClassType, type)) 
			and issubclass(obj, Tile) and obj is not Tile):
			for key in obj.KEY:
				assert key not in _key2tile, "Duplicate tile key for %r" % obj
				_key2tile[key] = obj

	def __init__(self, map_file):
		"""Construct a map from the map_file, which may be a resource
		file name or a file-like object

		>>> from StringIO import StringIO
		>>> map_file = StringIO(
		... '''name: test
		... description: A test map
		...
		... p w c C p
		...  b w m B
		... b m w c B
		... ''')
		>>> mp = Map(map_file)
		>>> mp.name
		'test'
		>>> mp.description
		'A test map'
		>>> mp.height
		3
		>>> len(mp.tiles)
		3
		>>> mp.width
		5
		>>> len(mp.tiles[0])
		5
		>>> tile_lines = iter(mp)
		>>> [t.__class__.__name__ for t in tile_lines.next()]
		['Mountain', 'Water', 'CoveredGround', 'CoveredGround', 'Mountain']
		>>> [t.__class__.__name__ for t in tile_lines.next()]
		['BareGround', 'Water', 'Minerals', 'BareGround']
		>>> [t.__class__.__name__ for t in tile_lines.next()]
		['BareGround', 'Minerals', 'Water', 'CoveredGround', 'BareGround']
		>>> mp.tile(0, 0).above
		>>> mp.tile(0, 2).above is mp.tile(0, 1)
		True
		>>> mp.tile(2, 0).above_right
		>>> mp.tile(3, 2).above_right is mp.tile(4, 1)
		True
		>>> mp.tile(4, 2).right
		>>> mp.tile(2, 2).right is mp.tile(3, 2)
		True
		>>> mp.tile(3, 2).below_right
		>>> mp.tile(2, 1).below_right is mp.tile(3, 2)
		True
		>>> mp.tile(2, 2).below
		>>> mp.tile(0, 1).below is mp.tile(0, 2)
		True
		>>> mp.tile(0, 1).below_left
		>>> mp.tile(1, 1).below_left is mp.tile(0, 2)
		True
		>>> mp.tile(0, 1).left
		>>> mp.tile(1, 1).left is mp.tile(0, 1)
		True
		>>> mp.tile(1, 0).above_left
		>>> mp.tile(1, 1).above_left is mp.tile(0, 0)
		True
		"""
		if isinstance(map_file, str):
			map_file = pyglet.resource.file(map_file)
		data = rfc822.Message(map_file)
		self.name = data['name']
		self.description = data['description']
		self.rand_seed = data.get('random-seed') or self.name
		random.seed(self.rand_seed)
		self.create_tiles(map_file)
		self.set_tile_neighbors()
		self.set_nodes()
		random.seed()
	
	def tile(self, x, y, z=None):
		"""Return the tile at the specified location or None if the location
		is out of bounds
		"""
		if x >= 0 and y >= 0:
			try:
				return self.tiles[int(round(y))][int(round(x))]
			except IndexError:
				return None
	
	def create_tiles(self, key_lines):
		"""Create a tile grid from the lines of character tile keys specified"""
		line_no = 1
		self.tiles = []
		for line in key_lines:
			line = line.strip().replace(' ', '')
			tile_line = [self._key2tile[key](key) for key in line]
			self.tiles.append(tile_line)
			if line_no == 1:
				self.width = len(tile_line)
			assert len(line) == self.width, (
				"Error in map %s line %s, line width does not match map width"
				% (self.name, line_no))
			line_no += 1
		self.height = len(self.tiles)
		self.tiles.reverse()
	
	def set_tile_neighbors(self):
		"""Initialize tile neighbor references"""
		for y in range(self.height):
			for x in range(self.width):
				tile = self.tile(x, y)
				assert tile, "Error in map %s no tile found at %s, %s" % (self.name, x, y)
				tile.set_position(x, y)
				tile.set_neighbors(
					self.tile(x, y+1),
					self.tile(x+1, y+1),
					self.tile(x+1, y),
					self.tile(x+1, y-1),
					self.tile(x, y-1),
					self.tile(x-1, y-1),
					self.tile(x-1, y),
					self.tile(x-1, y+1),
					)

	def set_node_neighbors(self, nodes, testfunc):
		for node in nodes.values():
			node.neighborz = [
				nodes[t.position] 
				for t in [node.tile.above, node.tile.right, node.tile.below, node.tile.left]
				if testfunc(t, node)
			]
			for t, (neighbor0, neighbor1) in (
					(node.tile.above_right, (node.tile.above, node.tile.right)),
					(node.tile.below_right, (node.tile.right, node.tile.below)),
					(node.tile.below_left, (node.tile.below, node.tile.left)),
					(node.tile.above_left, (node.tile.left, node.tile.above)),
					):
				if testfunc(t, node) \
						and neighbor0.position in node.neighborz \
						and neighbor1.position in node.neighborz:
					node.neighborz.append(nodes[t.position])

	def set_nodes(self):
		self.ground_nodes = {}
		self.hover_nodes = {}
		self.flying_nodes = {}
		for row in self.tiles:
			for tile in row:
				self.ground_nodes[tile.position] = TileNode(tile, tile.GROUND_SPEED)
				self.hover_nodes[tile.position] = TileNode(tile, tile.HOVER_SPEED)
				self.flying_nodes[tile.position] = TileNode(tile, tile.FLYING_SPEED)
		self.set_node_neighbors(self.ground_nodes, 
			lambda t, node : t is not None and t.PASSABLE \
				and (t.elevation == node.tile.elevation or t.__class__ == Ramp))
		self.set_node_neighbors(self.hover_nodes, 
			lambda t, node : t is not None and t.HOVERABLE \
				and (t.elevation == node.tile.elevation or t.__class__ == Ramp))
		self.set_node_neighbors(self.flying_nodes, 
			lambda t, node : t is not None and t.FLYABLE)
	
	def __iter__(self):
		"""Iterate the horizontal lines of tiles, yields a list of
		tiles for each row in the map
		"""
		return iter(self.tiles)


class View:
	"""Map view base class
	
	(Will eventually be configurable)
	"""

	_tile_colors = {
		BareGround: (0.1, 0.12, 0),
		CoveredGround: (0.03, 0.2, 0.03),
		UnevenGround: (0.2, 0.2, 0.2),
		Boulder: (0.2, 0.1, 0),
		Ramp: (0.2, 0.2, 0.2),
		Minerals: (0.0, 0.5, 1.0),
		GasVent: (0.0, 1.0, 0.5),
		Water: (0.0, 0.2, 0.6),
		Mountain: (0.05, 0.05, 0.07),
		}
	
	_default_tile_tex = pyglet.resource.texture('uneven-ground.png')
	
	_tile_textures = {
		BareGround: pyglet.resource.texture('bare-ground.png'),
		CoveredGround: pyglet.resource.texture('covered-ground.png'),
		UnevenGround: pyglet.resource.texture('uneven-ground.png'),
		Boulder: pyglet.resource.texture('boulder.png'),
		Mountain: pyglet.resource.texture('mountain.png'),
		Water: pyglet.resource.texture('water.png'),
		Minerals: pyglet.resource.texture('minerals.png'),
	}

	_tile_types = sorted(_tile_textures.keys(), key=lambda t: t.PRIORITY)

	_splat_textures = {
		Tile.SOFT_EDGE: pyglet.resource.texture('splat-soft.png'),
		Tile.MEDIUM_EDGE: pyglet.resource.texture('splat.png'),
		Tile.HARD_EDGE: pyglet.resource.texture('splat-hard.png'),
	}


def mode(*values):
	most = 0
	result = None
	val2count = {}
	for v in values:
		if v in val2count:
			val2count[v] += 1
		else:
			val2count[v] = 1
		if val2count[v] > most:
			most = val2count[v]
			result = v
	return result


class TacticalViewChunk(View):
	"""Part of a tactical map view"""

	CHUNK_SIZE = 16 # Chunk width/height in tiles (must be power of two)
	TILE_SIZE = 1.0 # Width/height of each tile in viewport coords
	TERRAIN_DEPTH = 1.0 # Tile elevation multiplier
	SPLAT_SIZE = 8 # Tile splat resolution
	GROUND_TEX_SCALE = 0.2 # Ground texture coord scaling factor 
	NORMAL_DEVIATION = 0.02 # Random deviation of terrain normal vectors

	def __init__(self, map, tile_x, tile_y, tiles_wide, tiles_high):
		self.map = map
		self.tile_x = tile_x
		self.tile_y = tile_y
		self.tiles_wide = tiles_wide
		self.tiles_high = tiles_high
		self.build_vertex_list()
		self.create_splat_textures()

	def build_vertex_list(self):
		width = min(self.tiles_wide + 1, self.map.width - self.tile_x)
		height = min(self.tiles_high + 1, self.map.height -  self.tile_y)
		verts = []
		
		for ty in range(self.tile_y, self.tile_y + height):
			for tx in range(self.tile_x, self.tile_x + width):
				tile = self.map.tile(tx, ty)
				# tile vert
				x = tx * self.TILE_SIZE
				y = ty * self.TILE_SIZE
				if not tile.CLIP_ELEVATION:
					elevations = [tile.elevation, tile.elevation]
					for i in range(8):
						neighbor = tile.neighbors[i]
						if neighbor is not None:
							elevations.append(neighbor.elevation)
						z = sum(elevations)/len(elevations) * self.TERRAIN_DEPTH
				else:
					z = tile.elevation * self.TERRAIN_DEPTH
				verts.extend([x, y, z])
				if tx < (self.tile_x + width - 1):
					# intermediate vert to right
					elevations = [tile.elevation]
					for i in (0,1,2):
						neighbor = tile.neighbors[i]
						if neighbor is not None:
							elevations.append(neighbor.elevation)
					verts.extend(
						[x + self.TILE_SIZE * 0.5, y + self.TILE_SIZE * 0.5, 
							mode(*elevations) * self.TERRAIN_DEPTH])

		width = width * 2 - 1 # factor in intermediates

		def vert_height(x, y, w=width-1, h=height-1):
			"""Return the height of the given vert"""
			if w > x >= 0 and h > y >= 0:
				return verts[(y * width + x) * 3 + 2]
			else:
				return 1.0

		# Calculate normals and texture coords
		normals = []
		splat_tex_coords = []
		splat_scale = 1.0 / (self.CHUNK_SIZE * self.TILE_SIZE) * 0.98
		ground_tex_coords = []
		for ty in range(height):
			for tx in range(width):
				if tx > 0 and ty > 0 and tx < width - 1 and ty < height - 1:
					up = vector.Vec3(0, self.TILE_SIZE * 0.5, 
						 vert_height(tx, ty - 1) - vert_height(tx, ty))
					down = vector.Vec3(0, -self.TILE_SIZE * 0.5, 
						 vert_height(tx, ty + 1) - vert_height(tx, ty))
					left = vector.Vec3(-self.TILE_SIZE * 0.5, 0,
						 vert_height(tx - 1, ty) - vert_height(tx, ty))
					right = vector.Vec3(self.TILE_SIZE * 0.5, 0,
						 vert_height(tx + 1, ty) - vert_height(tx, ty))
					# Vert normal is the average of the normals of the
					# four surrounding triangles
					Ts = (up.cross(left), left.cross(down), down.cross(right), right.cross(up),
						vector.Vec3(random.gauss(0, self.NORMAL_DEVIATION),
							random.gauss(0, self.NORMAL_DEVIATION),
							random.gauss(0, self.NORMAL_DEVIATION))
						)
					N = vector.Vec3(
						sum(t.x for t in Ts), sum(t.y for t in Ts), sum(t.z for t in Ts))
					normals.extend(N.normalize())
				else:
					# Edge vert normals are always vertical
					normals.extend([0, 0, 1])
				y_offset = (tx % 2 != 0) * 0.5
				splat_tex_coords.extend([
					tx * 0.5 * splat_scale + 0.01,
					(ty + y_offset) * splat_scale + 0.01])
				ground_tex_coords.extend(
					[(tx + (self.tile_x * 2)) * self.GROUND_TEX_SCALE * 0.5,
					 (ty + self.tile_y + y_offset) * self.GROUND_TEX_SCALE * 0.5])

		# Setup triangle vertex indices in alternating diamond/square pattern
		# Setup texture coordinates for splat and ground textures
		indices = []
		for ty in range(height - 1):
			for tx in range(0, width - 2, 2):
				i = tx + ty * width
				indices.extend([
					i, i + 2, i + 1,
					i + 2, i + width + 2, i + 1,
					i, i + 1, i + width,
					i + width, i + 1, i + width + 2,
					])
		self.vertex_list = pyglet.graphics.vertex_list_indexed(width * height,
			indices, ('v3f/static', verts), ('n3f/static', normals), 
			('t2f/static', splat_tex_coords), ('t2f/static', ground_tex_coords))
		multitexture.enable(self.vertex_list)

	def draw(self):
		glColor3f(1, 1, 1)
		splat_tex = self._default_tile_tex
		glActiveTexture(GL_TEXTURE1)
		glClientActiveTexture(GL_TEXTURE1)
		glEnable(splat_tex.target)
		glBindTexture(splat_tex.target, splat_tex.id)
		self.vertex_list.draw(GL_TRIANGLES)
		glDisable(splat_tex.target)

		for tile_type in self._tile_types:
			if tile_type not in self._tile_splat_tex:
				continue # No tiles of that type in this chunk
			splat_tex = self._tile_splat_tex[tile_type]
			glActiveTexture(GL_TEXTURE0)
			glClientActiveTexture(GL_TEXTURE0)
			glEnable(splat_tex.target)
			glBindTexture(splat_tex.target, splat_tex.id)
			tile_tex = self._tile_textures[tile_type]
			glActiveTexture(GL_TEXTURE1)
			glClientActiveTexture(GL_TEXTURE1)
			glEnable(tile_tex.target)
			glBindTexture(tile_tex.target, tile_tex.id)
			if tile_type.CLIP_ELEVATION is not None:
				clip_z = tile_type.CLIP_ELEVATION + math.sin(game.state.time * 0.75)*0.1
				glClipPlane(GL_CLIP_PLANE0, 
					(ctypes.c_double * 4)(0, 0, -1, clip_z * self.TERRAIN_DEPTH))
				glEnable(GL_CLIP_PLANE0)
				glDisable(GL_DEPTH_TEST) # Avoid z-fighting
			self.vertex_list.draw(GL_TRIANGLES)
			if tile_type.CLIP_ELEVATION is not None:
				glDisable(GL_CLIP_PLANE0)
				glEnable(GL_DEPTH_TEST)
			glDisable(tile_tex.target)
		glActiveTexture(GL_TEXTURE0)
		glClientActiveTexture(GL_TEXTURE0)
		glDisable(splat_tex.target)
	
	def create_splat_textures(self):
		"""Generate the splay mask textures for all tile types"""
		buffers = pyglet.image.get_buffer_manager()
		self._tile_splat_tex = {}
		splat_tex_size = self.SPLAT_SIZE * self.CHUNK_SIZE
		for tile_type in self._tile_textures:
			# In theory textures at the edge could be smaller in some cases
			# but that requires rectangular texture support and will mean
			# the textures are not sized as a power of 2. Pyglet can handle
			# those cases, but it usually means the actual texture is just
			# scaled up to the nearest power of two size anyway and this
			# code and the texture coordinate scaling code end up more
			# complicated for no real gain
			tiles = self.render_splat_mask(tile_type)
			if tiles:
				splat_tex = pyglet.image.Texture.create(splat_tex_size, splat_tex_size)
				self._tile_splat_tex[tile_type] = splat_tex
				color_buffer = buffers.get_color_buffer()
				glReadBuffer(color_buffer.gl_buffer)
				glEnable(splat_tex.target)
				glBindTexture(splat_tex.target, splat_tex.id)
				glCopyTexImage2D(splat_tex.target, splat_tex.level,
					GL_INTENSITY, 0, 0, splat_tex_size, splat_tex_size, 0)
				glDisable(splat_tex.target)
		game.window.clear()
	
	def render_splat_mask(self, tile_type):
		"""Generate the mask texture for the specified tile type
		
		Return the tile count for the tile type in the chunk
		"""
		glDisable(GL_LIGHTING)
		glMatrixMode(GL_PROJECTION)
		glPushMatrix()
		glLoadIdentity()
		glOrtho(0, game.window.width, 0, game.window.height, -1, 1)
		glMatrixMode(GL_MODELVIEW)
		glPushMatrix()
		glLoadIdentity()
		game.window.clear()

		glEnable(GL_BLEND)
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
		splat_tex = self._splat_textures[tile_type.EDGE_BLEND]
		glEnable(splat_tex.target)
		glBindTexture(splat_tex.target, splat_tex.id)

		glTranslatef(self.tiles_wide * -self.TILE_SIZE/2.0,
			self.tiles_high * -self.TILE_SIZE/2.0, 0)
		glColor3f(1, 1, 1)
		size = self.SPLAT_SIZE * (tile_type.EDGE_OVERLAP + 1.5)
		count = 0
		for ty in range(-2, self.tiles_high + 2):
			for tx in range(-2, self.tiles_wide + 2):
				tile = self.map.tile(tx + self.tile_x, ty + self.tile_y)
				if isinstance(tile, tile_type):
					count += 1
					x = (tx * 0.98 + 0.02) * self.SPLAT_SIZE + self.SPLAT_SIZE * 0.25
					y = (ty * 0.98 + 0.02) * self.SPLAT_SIZE + self.SPLAT_SIZE * 0.25
					pyglet.graphics.draw(4, GL_QUADS,
						('v2f', (x, y, x + size, y, x + size, y + size, x, y + size)),
						('t2f', (0, 0, 1, 0, 1, 1, 0, 1)))
		glDisable(splat_tex.target)

		glPopMatrix()
		glMatrixMode(GL_PROJECTION)
		glPopMatrix()
		glMatrixMode(GL_MODELVIEW)
		glEnable(GL_LIGHTING)
		return count


class TacticalView:
	"""Large scale tactical view"""

	def __init__(self, map):
		self.map = map
		self.chunks = []
		chunk_tiles = TacticalViewChunk.CHUNK_SIZE
		chunks_wide = int(math.ceil(self.map.width / chunk_tiles))
		chunks_high = int(math.ceil(self.map.height / chunk_tiles))
		random.seed(map.rand_seed)
		assert chunks_wide and chunks_high, "Map %s is empty" % map.name
		for chunk_y in range(chunks_high):
			chunk_row = []
			y = chunk_y * chunk_tiles
			height = min(chunk_tiles, self.map.height - y) 
			if height > 1:
				for chunk_x in range(chunks_wide):
					x = chunk_x * chunk_tiles
					width = min(chunk_tiles, self.map.width - x)
					if width > 1:
						chunk_row.append(TacticalViewChunk(map, x, y, width, height))
				self.chunks.append(chunk_row)
		random.seed()

	def draw(self, controller):
		# Figure out which chunks are visible given 
		# the tacial tile dimensions and scroll position
		chunk_size = TacticalViewChunk.CHUNK_SIZE * TacticalViewChunk.TILE_SIZE
		scroll_x = controller.scroll_x
		scroll_y = controller.scroll_y
		chunk_left = int(scroll_x / chunk_size)
		chunk_right = int(math.ceil((scroll_x + controller.TILES_WIDE) / chunk_size))
		# Show the chunks above and below one tile early to cover any low and
		# high elevation tiles at the chunk borders. This is only required in the
		# vertical direction because of the tilt
		chunk_bottom = max(int((scroll_y - 1) / chunk_size), 0)
		chunk_top = int(math.ceil((scroll_y + controller.TILES_HIGH + 1) / chunk_size))
		for chunk_row in self.chunks[chunk_bottom:chunk_top]:
			for chunk in chunk_row[chunk_left:chunk_right]:
				chunk.draw()

	def map_coordinates(self, opengl_coordinates):
		"""Return the map coordinates for the given OpenGL coordinates.
		"""
		return vector.Vec3(
			opengl_coordinates[0] + (0.5 * self.map.width * TacticalViewChunk.TILE_SIZE),
			opengl_coordinates[1] + (0.25 * self.map.width * TacticalViewChunk.TILE_SIZE),
			opengl_coordinates[2])

	def opengl_coordinates(self, map_coordinates):
		"""Return the OpenGL coordinates for the given map coordinates.
		"""
		return vector.Vec3(
			map_coordinates[0] * TacticalViewChunk.TILE_SIZE,
			map_coordinates[1] * TacticalViewChunk.TILE_SIZE,
			map_coordinates[2])


class TileNode(astar.Node):

	def __init__(self, tile, speed_multiplier):
		self.tile = tile
		self.speed_multiplier = speed_multiplier

	def neighbors(self):
		return self.neighborz

	def distance(self, other):
		a = self.tile.location - other.tile.location
		return math.sqrt(a.dot(a))

	def cost(self, other):
		return self.distance(other) / self.speed_multiplier

	cost_estimate = distance


if __name__ == '__main__':
	import test
	test.run_doc_tests()
